뭘 만들지도 모른 채 시작해서, 돈 받는 프로덕트가 되기까지 14개월
2025년 5월, 뭘 만들지도 정하지 못한 채 시작한 프로젝트가 14개월 뒤 결제와 교사 매칭, AI 리포트 생성이 돌아가는 유료 프로덕트가 됐습니다. 그 사이에 아키텍처를 두 번 접었고, 상품 하나를 만들었다 제거했고, 장애를 겪었습니다. 만든 것보다 접은 것 중심으로 정리한 회고입니다.
Tag
2025년 5월, 뭘 만들지도 정하지 못한 채 시작한 프로젝트가 14개월 뒤 결제와 교사 매칭, AI 리포트 생성이 돌아가는 유료 프로덕트가 됐습니다. 그 사이에 아키텍처를 두 번 접었고, 상품 하나를 만들었다 제거했고, 장애를 겪었습니다. 만든 것보다 접은 것 중심으로 정리한 회고입니다.
Temporal 워크플로에 Spring AI를 붙이면서 내부를 들여다봤습니다. useProviderStructuredOutput()이 Anthropic 네이티브 outputConfig로 나가는 경로, 프롬프트 캐싱을 켰지만 read가 0이라 손해였던 이유, 그리고 .entity()가 가린 토큰·비용을 CallAdvisor로 거둔 방법입니다. 2부작 중 2편입니다.
진단 리포트 생성을 SQS+Lambda 이벤트 파이프라인으로 돌렸습니다. 동시성 한도, 큐 백로그 지연, 흩어진 오케스트레이션 세 가지가 걸렸고 이걸 Temporal 워크플로로 옮기면서 생성 로직을 한 메서드 안으로 모았습니다. 2부작 중 1편입니다.
Querydsl/JPA 기반 persistence를 Exposed DSL로 옮긴 작업은 단순한 ORM 교체가 아니었습니다. Kotlin 빌드에서 kapt와 생성 코드를 걷어내고, persistence 경계를 더 명시적으로 만든 전환이었습니다.
Architecture test는 설계를 대신해주는 도구가 아닙니다. 사람이 매번 확인하던 경계 위반을 빌드에서 먼저 드러내는 안전장치에 가깝습니다.
큰 도메인 모듈 하나로는 경계를 지키기 어렵습니다. s-class-backend에서 bounded context 기준으로 domain module을 나누며 배운 점을 정리합니다.
Spring Boot의 메타 어노테이션 원리를 활용해 @UseCase, @DomainService, @Adaptor 커스텀 어노테이션을 만들고, 레이어 아키텍처의 역할을 코드에 명시적으로 표현하는 방법을 도입 전/후 비교와 함께 정리합니다.
Spring Quartz 내부 구현을 따라가며 동적 스케줄링이 가능한 이유를 분석합니다. scheduleJob/rescheduleJob/deleteJob 흐름, QRTZ_* 테이블, Cluster 락, misfire 처리까지 실무 관점으로 정리합니다.
Native Query를 Kotlin JDSL로 마이그레이션하면서 확장 함수 패턴을 도입했습니다. 일반 join을 fetchJoin으로 변경하여 N+1 쿼리 문제를 해결하고, 데이터베이스 레벨에서 직접 집계 연산을 수행하여 쿼리 성능을 개선했습니다. Kotlin JDSL의 타입 안전성과 확장 함수 패턴을 활용하여 유지보수성을 크게 향상시켰습니다.
HikariCP 대시보드에서 Connection Timeout이 27회 발생하는 문제를 발견했습니다. 원인은 N+1 쿼리 문제와 작은 Connection Pool 크기였습니다. 배치 조회로 N+1 쿼리를 해결하고 Connection Pool 크기를 5에서 20으로 증가시켜 Timeout을 0으로 만들었습니다. 쿼리 수는 161개에서 3개로 98% 감소했습니다.
DDoS 공격을 겪고 나서 알게 된 모니터링의 가치. 실제 운영 환경에서 발생한 문제 상황을 바탕으로, Spring Boot에 Prometheus + Grafana 모니터링을 구축하는 과정을 스토리텔링 형식으로 공유합니다.
Multi-Repo 구조의 마이크로서비스에서 코드 중복 문제를 해결하기 위해 Common Package를 도입한 경험을 공유합니다. Mono-Repo vs Multi-Repo 고민, Common Package 설계 원칙, GitHub Packages 배포, 그리고 실제 적용 과정에서 얻은 교훈을 담았습니다.
마이크로서비스 환경에서 비즈니스 로직을 외부 의존성으로부터 보호하기 위해 헥사고날 아키텍처를 적용했습니다. Domain, Application, Adapter 계층으로 명확히 분리하고, Port와 Adapter 패턴으로 유연한 구조를 만든 실전 사례를 공유합니다.
여러 알림 채널(이메일, 카카오톡, Discord)을 지원하는 Notification Service에서 팩토리 패턴을 적용한 실전 사례입니다. 채널별로 다른 구현을 가진 Provider들을 Factory를 통해 동적으로 선택하여 확장하기 쉽고 유지보수하기 좋은 구조를 만든 과정을 공유합니다.
마이크로서비스 환경에서 여러 서비스를 거치는 결제 프로세스를 Saga 패턴으로 구현했습니다. PG 승인, Order 생성, 완료 처리의 각 단계에서 실패 시 보상 트랜잭션을 실행하여 데이터 일관성을 유지하는 실전 사례를 공유합니다.
마이크로서비스 아키텍처에서 동기식 HTTP 호출의 한계를 해결하기 위해 GCP Pub/Sub을 도입하여 비동기 알림 시스템을 구축했습니다. lms-service와 payment-service에서 notification-service로의 알림 전송을 Pub/Sub을 통해 처리하여 서비스 간 결합도를 낮추고 안정성을 높였습니다.
멀티테넌시 LMS 서비스를 구축하면서 Organization Subdomain 기반 라우팅과 도메인 설계를 통해 각 조직의 데이터를 안전하게 격리하는 아키텍처를 구현했습니다. OrganizationIdArgumentResolver를 통한 자동 OrganizationId 추출과 Shared Database 전략을 중심으로 설명합니다.
Spring Boot와 Hibernate를 사용하여 PostgreSQL의 jsonb 타입을 다룰 때 발생하는 타입 불일치 문제를 해결한 경험을 공유합니다. hypersistence-utils 라이브러리를 활용해 간단하고 안전하게 jsonb 컬럼을 Map 타입으로 매핑하는 방법을 설명합니다.